﻿using System;
using System.Linq;
using System.Reflection;
using System.Text;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Model.StructuredSource;
using log4net;

namespace Kaleida.ServiceMonitor.Model.Runtime
{
    internal class ScriptCompiler
    {
        private static readonly ILog log = LogManager.GetLogger(typeof (ScriptCompiler));
        private static readonly ReflectionOperationFactory defaultOperationFactory = new ReflectionOperationFactory(new[] { Assembly.GetExecutingAssembly() });

        private readonly IPreparedRequestFactory preparedRequestFactory ;
        private readonly IResponseHandlerFactory responseHandlerFactory ;

        public ScriptCompiler() : this(defaultOperationFactory, defaultOperationFactory)
        {
        }

        public ScriptCompiler([NotNull] IPreparedRequestFactory preparedRequestFactory, [NotNull] IResponseHandlerFactory responseHandlerFactory)
        {
            if (preparedRequestFactory == null) throw new ArgumentNullException("preparedRequestFactory");
            if (responseHandlerFactory == null) throw new ArgumentNullException("responseHandlerFactory");

            this.preparedRequestFactory = preparedRequestFactory;
            this.responseHandlerFactory = responseHandlerFactory;
        }

        internal CompiledScript Build(ScriptDefinition definition, string defaultName)
        {
        	var root = BuildScriptNode(definition, defaultName);

            var compiledScript = new CompiledScript(defaultName, root);

            if(log.IsDebugEnabled)
            {
                LogCompiledScriptContent(compiledScript);
            }
            
            return compiledScript;
        }

        private static void LogCompiledScriptContent(CompiledScript script)
        {
            var summary = new StringBuilder();
            summary.AppendFormat("Compiled script '{0}' ({1}) from structured source\r\n", script.Name, script.Id);

            summary.Append(BuildNodeSummary(script.Root));

            log.Debug(summary.ToString());
        }

        private static string BuildNodeSummary(CompiledNode compiledNode, string indent = "")
        {
            var summary = new StringBuilder();

            if(compiledNode.IsGroup)
            {
                foreach(var child in compiledNode.ScriptGroup.Children)
                {
                    summary.Append(BuildNodeSummary(child, indent + "  "));
                }
            }
            else
            {
                summary.AppendFormat("{0}Module {1} ({2})\r\n", indent, compiledNode.ScriptModule.Name, compiledNode.ScriptModule.Id);
            }

            return summary.ToString();
        }

        private CompiledNode BuildScriptNode(ScriptDefinition definition, string name)
    	{
    		return definition.ChildGroups.Any()
					? BuildScriptGroupNode(definition, name)
					: BuildScriptModuleNode(definition, name);
    	}

        private CompiledNode BuildScriptGroupNode(ScriptDefinition definition, string name)
    	{
    		var compiledNodes = (from i in definition.ChildGroups
								 let nameSuffix = definition.ChildGroups.Count() == 1 ? "" : "-" + (definition.ChildGroups.IndexOf(i) + 1).ToString("0")
								 select BuildScriptNode(i, name + nameSuffix)).ToList();

    		return new CompiledNode(new CompiledGroup(compiledNodes));
    	}

        private CompiledNode BuildScriptModuleNode(ScriptDefinition definition, string defaultName)
    	{
			if (definition.ChildGroups.Any())
				throw new InvalidOperationException("Cannot build ScriptModule from grouping definition");

			var module = new CompiledScriptModule(defaultName);
    		foreach (var operation in definition.Operations)
    		{
    			try
    			{
    				var preparedRequest = preparedRequestFactory.Build(operation.RequestAction.Name, operation.RequestAction.Arguments);
    				var responseHandler = responseHandlerFactory.Build(operation.HandlerAction.Name, operation.HandlerAction.Arguments);
    				
    				module.Operations.Add(new Operation(preparedRequest, responseHandler));
    			}
    			catch (Exception exception)
    			{
    				throw new InvalidOperationException(string.Format("Could not build operation {0} of {1}: {2}", definition.Operations.IndexOf(operation), definition.Operations.Count(), exception.Message));
    			}
    		}

			var processor = new DirectiveProcessor();
            CompiledNode node = processor.Apply(module, definition.Directives);
            // An #include directive may change the node type from module to group

            return node;
    	}
    }
}
